diff --git a/algo/CMakeLists.txt b/algo/CMakeLists.txt
index 69f1657da64d48125dda6e99f2a9316de6ce2cf5..0f75be2c14fcf920d42b89f47e5f424a347dfd3f 100644
--- a/algo/CMakeLists.txt
+++ b/algo/CMakeLists.txt
@@ -6,6 +6,7 @@ add_subdirectory (data)
 
 set(SRCS
   evbuild/EventBuilder.cxx
+  trigger/TimeClusterTrigger.cxx
  )
 
 add_library(Algo SHARED ${SRCS})
diff --git a/algo/trigger/TimeClusterTrigger.cxx b/algo/trigger/TimeClusterTrigger.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..21ed7f8b288dff0b8a67b9a82cfaf52649851ad3
--- /dev/null
+++ b/algo/trigger/TimeClusterTrigger.cxx
@@ -0,0 +1,52 @@
+/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer], Dominik Smith */
+
+#include "TimeClusterTrigger.h"
+
+#include <algorithm>
+#include <cassert>
+#include <iterator>
+#include <vector>
+
+using std::vector;
+
+namespace cbm::algo
+{
+
+  vector<double> TimeClusterTrigger::operator()(const vector<double>& dataVec, double winSize, int32_t minNumData,
+                                                double deadTime) const
+  {
+
+    assert(std::is_sorted(dataVec.begin(), dataVec.end()));
+
+    vector<double> triggerVec;
+    auto winStart = dataVec.begin();
+    auto current  = dataVec.begin();
+
+    while (current != dataVec.end()) {
+
+      // If window size is exceeded, adjust window start
+      while (*current - *winStart > winSize)
+        winStart++;
+
+      // Create trigger if threshold is reached
+      if (std::distance(winStart, current) >= minNumData - 1) {
+        triggerVec.push_back(0.5 * (*current + *winStart));
+
+        // Start new window after dead time
+        winStart = current + 1;
+        while (winStart != dataVec.end() && *winStart - *current < deadTime)
+          winStart++;
+        current = winStart;
+      }
+
+      // If threshold is not reached, check with next element
+      else
+        current++;
+    }
+
+    return triggerVec;
+  }
+
+}  // namespace cbm::algo
diff --git a/algo/trigger/TimeClusterTrigger.h b/algo/trigger/TimeClusterTrigger.h
new file mode 100644
index 0000000000000000000000000000000000000000..e3794533e8a4af59bdbdfcdb6adb7ecafd5ed525
--- /dev/null
+++ b/algo/trigger/TimeClusterTrigger.h
@@ -0,0 +1,43 @@
+/* Copyright (C) 2021 GSI Helmholtzzentrum fuer Schwerionenforschung, Darmstadt
+   SPDX-License-Identifier: GPL-3.0-only
+   Authors: Volker Friese [committer], Dominik Smith */
+
+#ifndef CBM_ALGO_TIMECLUSTERTRIGGER_H
+#define CBM_ALGO_TIMECLUSTERTRIGGER_H 1
+
+
+#include <cstdint>
+#include <vector>
+
+namespace cbm::algo
+{
+
+  /** @class TimeClusterTrigger
+   ** @author Volker Friese <v.friese@gsi.de>
+   ** @author Dominik Smith <d.smith@gsi.de>
+   ** @since 2021
+   ** @brief Finds clusters in time-series data
+   **
+   ** A trigger is generated when the number density of data exceeds a given threshold. Each datum
+   ** can contribute to only one trigger. Consecutive triggers are separated by at least the dead time.
+   **
+   ** The input vector must be sorted, otherwise the behaviour is undefined.
+   **/
+  class TimeClusterTrigger {
+
+  public:
+    /** @brief Execution
+     ** @param  dataVec     Source data vector
+     ** @param  winSize     Size of trigger window
+     ** @param  minNumData  Threshold on number of data within the trigger window
+     ** @param  deadTime    Minimum time between two triggers
+     ** @return Vector of trigger times
+     **/
+    std::vector<double> operator()(const std::vector<double>& dataVec, double winSize, int32_t minNumData,
+                                   double deadTime) const;
+  };
+
+
+}  // namespace cbm::algo
+
+#endif /* CBM_ALGO_TIMECLUSTERTRIGGER_H */