diff --git a/corsika/detail/modules/thinning/EMThinning.inl b/corsika/detail/modules/thinning/EMThinning.inl
new file mode 100644
index 0000000000000000000000000000000000000000..29ed5ad46d2268700b505fde6f386782cf06e8bc
--- /dev/null
+++ b/corsika/detail/modules/thinning/EMThinning.inl
@@ -0,0 +1,90 @@
+/*
+ * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#pragma once
+
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/process/SecondariesProcess.hpp>
+
+namespace corsika {
+
+  EMThinning::EMThinning(HEPEnergyType threshold, double maxWeight, bool const eraseParticles)
+      : threshold_{threshold}
+      , maxWeight_{maxWeight}
+      , eraseParticles_ {eraseParticles} {}
+
+  template <typename TStackView>
+  void EMThinning::doSecondaries(TStackView& view) {
+    if (view.getSize() != 2) return;
+
+    auto projectile = view.getProjectile();
+    if (!is_em(projectile.getPID())) return;
+
+    double const parentWeight = projectile.getWeight();
+    if (parentWeight >= maxWeight_) { return; }
+
+    auto particle1 = view.begin();
+    auto particle2 = ++(view.begin());
+
+    // skip non-EM interactions
+    if (!is_em(particle1.getPID()) || !is_em(particle2.getPID())) { return; }
+
+    /* skip high-energy interactions
+     * We consider only the primary energy. A more sophisticated algorithm might
+     * sort out above-threshold secondaries and apply thinning only on the subset of
+     * those below the threshold.
+     */
+    if (projectile.getEnergy() > threshold_) { return; }
+
+    corsika::HEPEnergyType const Esum = particle1.getEnergy() + particle2.getEnergy();
+
+    double const p1 = particle1.getEnergy() / Esum;
+    double const p2 = particle2.getEnergy() / Esum;
+
+    // weight factors
+    double const w1 = parentWeight / p1;
+    double const w2 = parentWeight / p2;
+
+    // max. allowed weight factor
+    double const maxWeightFactor = maxWeight_ / parentWeight;
+
+    if (w1 <= maxWeightFactor && w2 <= maxWeightFactor) { // apply Hillas thinning
+      if (uniform_(rng_) <= p1) {                         // keep 1st with probability p1
+        particle2.setWeight(0);
+        particle1.setWeight(w1 * parentWeight);
+      } else { // keep 2nd
+        particle1.setWeight(0);
+        particle2.setWeight(w2 * parentWeight);
+      }
+    } else { // weight limitation kicks in, do statistical thinning
+      double const w1prime = std::min(w1, maxWeightFactor);
+      double const w2prime = std::min(w2, maxWeightFactor);
+
+      if (uniform_(rng_) * w1prime <= parentWeight) { // keep 1st
+        particle1.setWeight(w1prime * parentWeight);
+      } else {
+        particle1.setWeight(0);
+      }
+      if (uniform_(rng_) * w2prime <= parentWeight) { // keep 2nd
+        particle2.setWeight(w2prime * parentWeight);
+      } else {
+        particle2.setWeight(0);
+      }
+    }
+
+    // erase discared particles in case of multithinning
+    if (eraseParticles_) {
+                for (auto& p : view) {
+                    if (auto const w = p.getWeight(); w == 0) { p.erase(); }
+                }
+        } else {
+        return;
+    }
+  }
+
+} // namespace corsika
diff --git a/corsika/modules/thinning/EMThinning.hpp b/corsika/modules/thinning/EMThinning.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..a32c05a6345d533217c0b1653b8ecfb6886d5308
--- /dev/null
+++ b/corsika/modules/thinning/EMThinning.hpp
@@ -0,0 +1,57 @@
+/*
+ * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#pragma once
+
+#include <random>
+
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/random/RNGManager.hpp>
+#include <corsika/framework/process/SecondariesProcess.hpp>
+
+namespace corsika {
+
+  /**
+   * This process implements thinning for EM splitting processes (1 -> 2).
+   */
+
+  class EMThinning : public SecondariesProcess<EMThinning> {
+  public:
+    /**
+     * Construct a new EMThinning process.
+     *
+     * @param threshold: thinning applied below this energy
+     * @param maxWeight: maximum allowed weight
+     */
+    EMThinning(HEPEnergyType threshold, double maxWeight, bool const eraseParticles=true);
+
+    /**
+     * Apply thinning to secondaries. Only EM primaries with two EM secondaries are
+     * considered.
+     *
+     * If the maximum weight is still out of reach, Hillas thinning is applied (i.e.
+     * one of the two secondaries is kept, the other one discarded). If the acceptance
+     * probabilities would lead to a weight factor exceeding the maximum weight, we resort
+     * to statistical thinning (i.e. the secondaries are kept/discared randomly each on is
+     * own). In that case, acceptance probabilities can be assigned without constraints
+     * (sum does not need to be 1) and we increase the acceptance probability such that
+     * the maximum weight is not exceeded.
+     */
+    template <typename TStackView>
+    void doSecondaries(TStackView&);
+
+  private:
+    default_prng_type& rng_ = RNGManager<>::getInstance().getRandomStream("thinning");
+    std::uniform_real_distribution<double> uniform_{};
+    HEPEnergyType const threshold_;
+    double const maxWeight_;
+    bool const eraseParticles_;
+  };
+} // namespace corsika
+
+#include <corsika/detail/modules/thinning/EMThinning.inl>
diff --git a/tests/common/SetupStack.hpp b/tests/common/SetupStack.hpp
index e2bd85f2aeaa903f65854357d5a51834f43f0c57..4a7ceff3ff4590f1bd20a8736aec0ddc4581d704 100644
--- a/tests/common/SetupStack.hpp
+++ b/tests/common/SetupStack.hpp
@@ -31,11 +31,11 @@ namespace corsika::test {
   /*
    * the version without history
    */
-  using Stack = detail::StackWithGeometry;
+  using Stack = detail::StackWithWeight;
 
 #endif
 
   // the correct secondary stack view
   using StackView = typename Stack::stack_view_type;
 
-} // namespace corsika::test
\ No newline at end of file
+} // namespace corsika::test
diff --git a/tests/common/TestStack.hpp b/tests/common/TestStack.hpp
index 2488fc1f059cfa4b4df6ed27a931feff60a62606..934575cb1deef376109b8dcd7c55d1c06e4b1533 100644
--- a/tests/common/TestStack.hpp
+++ b/tests/common/TestStack.hpp
@@ -48,20 +48,33 @@ namespace corsika {
                       node::GeometryData<DummyEnvironment>, StackWithGeometryInterface,
                       DefaultSecondaryProducer>;
 
+    template <typename TStackIter>
+    using SetupWeightDataInterface =
+        typename weights::MakeWeightDataInterface<TStackIter>::type;
+
+    template <typename TStackIter>
+    using StackWithWeightInterface =
+        CombinedParticleInterface<StackWithGeometry::pi_type, SetupWeightDataInterface,
+                                  TStackIter>;
+
+    using StackWithWeight =
+        CombinedStack<typename StackWithGeometry::stack_data_type, weights::WeightData,
+                      StackWithWeightInterface, DefaultSecondaryProducer>;
+
     // ------------------------------------------
     // Add [optional] history data to stack, too:
 
     // combine dummy stack with geometry information for tracking
     template <typename TStackIter>
     using StackWithHistoryInterface =
-        CombinedParticleInterface<StackWithGeometry::pi_type,
+        CombinedParticleInterface<StackWithWeight::pi_type,
                                   history::HistoryEventDataInterface, TStackIter>;
 
     using StackWithHistory =
-        CombinedStack<typename StackWithGeometry::stack_data_type,
+        CombinedStack<typename StackWithWeight::stack_data_type,
                       history::HistoryEventData, StackWithHistoryInterface,
                       history::HistorySecondaryProducer>;
 
   } // namespace test::detail
 
-} // namespace corsika
\ No newline at end of file
+} // namespace corsika
diff --git a/tests/modules/CMakeLists.txt b/tests/modules/CMakeLists.txt
index 3bb6cf8db10c42d58b4862d01dde1e297b07ea2b..015093588f7ec42d0496ba5bb0d323badc425f4d 100644
--- a/tests/modules/CMakeLists.txt
+++ b/tests/modules/CMakeLists.txt
@@ -13,6 +13,7 @@ set (test_modules_sources
   testSibyll.cpp
   testEpos.cpp
   testRadio.cpp
+  testEMThinning.cpp
   testSophia.cpp
   )
 
diff --git a/tests/modules/testEMThinning.cpp b/tests/modules/testEMThinning.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..9b23d0c129d94fcce3c63873b7af8254ffe6eae5
--- /dev/null
+++ b/tests/modules/testEMThinning.cpp
@@ -0,0 +1,223 @@
+/*
+ * (c) Copyright 2022 CORSIKA Project, corsika-project@lists.kit.edu
+ *
+ * This software is distributed under the terms of the GNU General Public
+ * Licence version 3 (GPL Version 3). See file LICENSE for a full version of
+ * the license.
+ */
+
+#include <corsika/framework/core/PhysicalUnits.hpp>
+#include <corsika/framework/geometry/Point.hpp>
+#include <corsika/framework/geometry/Vector.hpp>
+#include <corsika/framework/utility/CorsikaFenv.hpp>
+#include <corsika/media/Environment.hpp>
+#include <corsika/modules/thinning/EMThinning.hpp>
+
+#include <SetupStack.hpp>
+#include <SetupTestTrajectory.hpp>
+#include <SetupTestEnvironment.hpp>
+
+#include <catch2/catch.hpp>
+
+#include <boost/accumulators/accumulators.hpp>
+#include <boost/accumulators/statistics/stats.hpp>
+#include <boost/accumulators/statistics/mean.hpp>
+
+using namespace corsika;
+
+using DummyEnvironmentInterface = IMediumPropertyModel<IMagneticFieldModel<IMediumModel>>;
+using DummyEnvironment = Environment<DummyEnvironmentInterface>;
+
+TEST_CASE("EMThinning", "process,secondary") {
+  RNGManager<>::getInstance().registerRandomStream("thinning");
+  logging::set_level(logging::level::info);
+
+  feenableexcept(FE_INVALID);
+  using EnvType = DummyEnvironment;
+
+  EnvType env;
+  CoordinateSystemPtr const& rootCS = env.getCoordinateSystem();
+
+  // setup empty particle stack
+  test::Stack stack;
+  stack.clear();
+
+  SECTION("non-EM primary") {
+    auto prim = stack.addParticle(std::make_tuple(Code::Proton, 10_GeV,
+                                                  DirectionVector(rootCS, {1, 0, 0}),
+                                                  Point(rootCS, 0_m, 0_m, 0_m), 0_ns));
+    test::StackView view{prim};
+    auto projectile = view.getProjectile();
+
+    auto sec1 = projectile.addSecondary(
+        std::make_tuple(Code::PiPlus, 2_GeV, DirectionVector(rootCS, {1, 0, 0})));
+    auto sec2 = projectile.addSecondary(
+        std::make_tuple(Code::KMinus, 8_GeV, DirectionVector(rootCS, {1, 0, 0})));
+
+    EMThinning thinning{100_GeV, 1e20};
+    thinning.doSecondaries(view);
+    REQUIRE(view.getEntries() == 2);
+    REQUIRE(sec1.getWeight() == 1.);
+    REQUIRE(sec2.getWeight() == 1.);
+  }
+
+  SECTION("non-EM interaction") {
+    auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV,
+                                                  DirectionVector(rootCS, {1, 0, 0}),
+                                                  Point(rootCS, 0_m, 0_m, 0_m), 0_ns));
+    test::StackView view{prim};
+    auto projectile = view.getProjectile();
+
+    // mimic photohadronic interaction, more than 2 secondaries
+    auto sec1 = projectile.addSecondary(
+        std::make_tuple(Code::PiPlus, 2_GeV, DirectionVector(rootCS, {1, 0, 0})));
+    auto sec2 = projectile.addSecondary(
+        std::make_tuple(Code::KMinus, 3_GeV, DirectionVector(rootCS, {1, 0, 0})));
+    auto sec3 = projectile.addSecondary(
+        std::make_tuple(Code::Proton, 5_GeV, DirectionVector(rootCS, {1, 0, 0})));
+
+    EMThinning thinning{100_GeV, 1e20};
+    thinning.doSecondaries(view);
+    REQUIRE(view.getEntries() == 3);
+    REQUIRE(sec1.getWeight() == 1.);
+    REQUIRE(sec2.getWeight() == 1.);
+    REQUIRE(sec3.getWeight() == 1.);
+  }
+
+  SECTION("above threshold") {
+    auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV,
+                                                  DirectionVector(rootCS, {1, 0, 0}),
+                                                  Point(rootCS, 0_m, 0_m, 0_m), 0_ns));
+
+    test::StackView view{prim};
+    auto projectile = view.getProjectile();
+
+    auto sec1 = projectile.addSecondary(
+        std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0})));
+    auto sec2 = projectile.addSecondary(
+        std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0})));
+
+    EMThinning thinning{1_MeV, 1e20};
+    thinning.doSecondaries(view);
+    REQUIRE(view.getEntries() == 2);
+    REQUIRE(sec1.getWeight() == 1.);
+    REQUIRE(sec2.getWeight() == 1.);
+  }
+
+  SECTION("below threshold") {
+    EMThinning thinning{100_GeV, 1e20};
+
+    boost::accumulators::accumulator_set<
+        double, boost::accumulators::stats<boost::accumulators::tag::mean>>
+        acc;
+    boost::accumulators::accumulator_set<
+        HEPEnergyType, boost::accumulators::stats<boost::accumulators::tag::mean>>
+        accE;
+
+    // statistical test, 10000 iterations
+    for (int i = 0; i < 10'000; ++i) {
+      stack.clear();
+      auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV,
+                                                    DirectionVector(rootCS, {1, 0, 0}),
+                                                    Point(rootCS, 0_m, 0_m, 0_m), 0_ns));
+
+      test::StackView view{prim};
+      auto projectile = view.getProjectile();
+
+      projectile.addSecondary(
+          std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0})));
+      projectile.addSecondary(
+          std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0})));
+
+      thinning.doSecondaries(view);
+      REQUIRE(view.getEntries() == 1);
+
+      double sumWeight{};
+      HEPEnergyType sumE{};
+
+      for (auto& p : view) {
+        sumWeight += p.getWeight();
+        sumE += p.getWeight() * p.getEnergy();
+      }
+
+      acc(sumWeight);
+      accE(sumE);
+    }
+
+    REQUIRE(boost::accumulators::mean(acc) == Approx(2).epsilon(2e-2));
+    REQUIRE(boost::accumulators::mean(accE) / 1_GeV == Approx(10).epsilon(2e-2));
+  }
+
+  SECTION("weight limitation") {
+    double constexpr maxWeight = 3; // min acceptance prob. >= 0.3333
+    EMThinning thinning{100_GeV, maxWeight};
+
+    boost::accumulators::accumulator_set<
+        double, boost::accumulators::stats<boost::accumulators::tag::mean>>
+        acc;
+    boost::accumulators::accumulator_set<
+        HEPEnergyType, boost::accumulators::stats<boost::accumulators::tag::mean>>
+        accE;
+
+    // statistical test, 10000 iterations
+    for (int i = 0; i < 10'000; ++i) {
+      stack.clear();
+      auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV,
+                                                    DirectionVector(rootCS, {1, 0, 0}),
+                                                    Point(rootCS, 0_m, 0_m, 0_m), 0_ns));
+
+      test::StackView view{prim};
+      auto projectile = view.getProjectile();
+
+      projectile.addSecondary(
+          std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0})));
+      projectile.addSecondary(
+          std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0})));
+
+      thinning.doSecondaries(view);
+
+      double sumWeight{};
+      HEPEnergyType sumE{};
+
+      for (auto& p : view) {
+        auto const w = p.getWeight();
+        REQUIRE(w <= maxWeight);
+        sumWeight += w;
+        sumE += p.getEnergy() * w;
+      }
+
+      acc(sumWeight);
+      accE(sumE);
+    }
+
+    REQUIRE(boost::accumulators::mean(acc) == Approx(2).epsilon(2e-2));
+    REQUIRE(boost::accumulators::mean(accE) / 1_GeV == Approx(10).epsilon(2e-2));
+  }
+
+  SECTION("max. weight reached") {
+    double constexpr maxWeight = 3; // min acceptance prob. >= 0.3333
+    EMThinning thinning{100_GeV, maxWeight};
+
+    for (int i = 0; i < 10'000; ++i) {
+      stack.clear();
+      auto prim = stack.addParticle(std::make_tuple(Code::Photon, 10_GeV,
+                                                    DirectionVector(rootCS, {1, 0, 0}),
+                                                    Point(rootCS, 0_m, 0_m, 0_m), 0_ns));
+      prim.setWeight(3.5);
+
+      test::StackView view{prim};
+      auto projectile = view.getProjectile();
+
+      auto sec1 = projectile.addSecondary(
+          std::make_tuple(Code::Electron, 2_GeV, DirectionVector(rootCS, {1, 0, 0})));
+      auto sec2 = projectile.addSecondary(
+          std::make_tuple(Code::Positron, 8_GeV, DirectionVector(rootCS, {1, 0, 0})));
+
+      // cannot perform thinning anymore, always keep all secondaries, weight unchanged
+      thinning.doSecondaries(view);
+      REQUIRE(view.getEntries() == 2);
+      REQUIRE(sec1.getWeight() == 3.5);
+      REQUIRE(sec2.getWeight() == 3.5);
+    }
+  }
+}