diff --git a/app/models/email.rb b/app/models/email.rb
index 5c74dce73083003828c623e5e84a114585d09bde..c2e6919322358d950a0bf416ad8617c468b25fbe 100644
--- a/app/models/email.rb
+++ b/app/models/email.rb
@@ -11,6 +11,12 @@ class Email < ApplicationRecord
 
   validate :unique_email, if: ->(email) { email.email_changed? }
 
+  scope :users_by_detumbled_email_count, ->(email) do
+    normalized_email = ::Gitlab::Utils::Email.normalize_email(email)
+
+    where(detumbled_email: normalized_email).distinct.count(:user_id)
+  end
+
   scope :confirmed, -> { where.not(confirmed_at: nil) }
   scope :unconfirmed, -> { where(confirmed_at: nil) }
   scope :unconfirmed_and_created_before, ->(created_cut_off) { unconfirmed.where('created_at < ?', created_cut_off) }
diff --git a/app/models/user.rb b/app/models/user.rb
index 866fc089b297c58fff6325f9072b721816d2b26b..c98b72bf1c8e1052151ad03644e1cf69dc60f234 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -341,6 +341,7 @@ def update_tracked_fields!(request)
   validate :namespace_move_dir_allowed, if: :username_changed?, unless: :new_record?
 
   validate :unique_email, if: :email_changed?
+  validates_with AntiAbuse::UniqueDetumbledEmailValidator, if: :email_changed?
   validate :notification_email_verified, if: :notification_email_changed?
   validate :public_email_verified, if: :public_email_changed?
   validate :commit_email_verified, if: :commit_email_changed?
@@ -1253,15 +1254,6 @@ def unique_email
 
       errors.add(:email, _('is linked to an account pending deletion.'), help_page_url: help_page_url)
     end
-
-    banned_user_email_reuse_check unless errors.include?(:email)
-  end
-
-  def banned_user_email_reuse_check
-    return unless ::Feature.enabled?(:block_banned_user_normalized_email_reuse, ::Feature.current_request)
-    return unless ::Users::BannedUser.by_detumbled_email(email).exists?
-
-    errors.add(:email, _('is not allowed. Please enter a different email address and try again.'))
   end
 
   def commit_email_or_default
diff --git a/app/validators/anti_abuse/unique_detumbled_email_validator.rb b/app/validators/anti_abuse/unique_detumbled_email_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e471b49b4b899be8cc8cbb5d1606357a70f17b91
--- /dev/null
+++ b/app/validators/anti_abuse/unique_detumbled_email_validator.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module AntiAbuse
+  class UniqueDetumbledEmailValidator < ActiveModel::Validator
+    NORMALIZED_EMAIL_ACCOUNT_LIMIT = 5
+
+    def validate(record)
+      return if record.errors.include?(:email)
+
+      email = record.email
+
+      return unless prevent_banned_user_email_reuse?(email) || limit_normalized_email_reuse?(email)
+
+      record.errors.add(:email, _('is not allowed. Please enter a different email address and try again.'))
+    end
+
+    private
+
+    def prevent_banned_user_email_reuse?(email)
+      return false unless ::Feature.enabled?(:block_banned_user_normalized_email_reuse, ::Feature.current_request)
+
+      ::Users::BannedUser.by_detumbled_email(email).exists?
+    end
+
+    def limit_normalized_email_reuse?(email)
+      return false unless ::Feature.enabled?(:limit_normalized_email_reuse, ::Feature.current_request)
+
+      Email.users_by_detumbled_email_count(email) >= NORMALIZED_EMAIL_ACCOUNT_LIMIT
+    end
+  end
+end
+
+AntiAbuse::UniqueDetumbledEmailValidator.prepend_mod
diff --git a/config/feature_flags/gitlab_com_derisk/limit_normalized_email_reuse.yml b/config/feature_flags/gitlab_com_derisk/limit_normalized_email_reuse.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d9001d6c10f0fe34e43f3cd15017542c025e7c32
--- /dev/null
+++ b/config/feature_flags/gitlab_com_derisk/limit_normalized_email_reuse.yml
@@ -0,0 +1,9 @@
+---
+name: limit_normalized_email_reuse
+feature_issue_url: https://gitlab.com/gitlab-org/modelops/anti-abuse/team-tasks/-/issues/812
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167357
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/495124
+milestone: '17.6'
+group: group::anti-abuse
+type: gitlab_com_derisk
+default_enabled: false
diff --git a/ee/app/validators/ee/anti_abuse/unique_detumbled_email_validator.rb b/ee/app/validators/ee/anti_abuse/unique_detumbled_email_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d63a5e79e4469c3beac527dcf1e0ab1ba5735ce0
--- /dev/null
+++ b/ee/app/validators/ee/anti_abuse/unique_detumbled_email_validator.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module EE
+  module AntiAbuse
+    module UniqueDetumbledEmailValidator
+      extend ::Gitlab::Utils::Override
+
+      private
+
+      override :limit_normalized_email_reuse?
+      def limit_normalized_email_reuse?(email)
+        super && !paid_verified_domain?(email)
+      end
+
+      def paid_verified_domain?(email)
+        return false unless ::Gitlab::Saas.feature_available?(:limit_normalized_email_reuse)
+
+        email_domain = Mail::Address.new(email).domain.downcase
+
+        return true if email_domain == ::Gitlab::Saas.root_domain
+
+        root_group = ::PagesDomain.verified.find_by_domain_case_insensitive(email_domain)&.root_group
+
+        !!root_group&.paid?
+      end
+    end
+  end
+end
diff --git a/ee/config/saas_features/limit_normalized_email_reuse.yml b/ee/config/saas_features/limit_normalized_email_reuse.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5159a0df42ee129800baeb19fdbba10ca6c04b93
--- /dev/null
+++ b/ee/config/saas_features/limit_normalized_email_reuse.yml
@@ -0,0 +1,5 @@
+---
+name: limit_normalized_email_reuse
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/167357
+milestone: '17.6'
+group: group::anti-abuse
diff --git a/ee/lib/ee/gitlab/saas.rb b/ee/lib/ee/gitlab/saas.rb
index d86fcbf795ce89521cb84788be9cff56d9fe297c..8f9928d18e71165e575dc2f0bc55217967e15908 100644
--- a/ee/lib/ee/gitlab/saas.rb
+++ b/ee/lib/ee/gitlab/saas.rb
@@ -32,6 +32,7 @@ module Saas
           gitlab_duo_saas_only
           pipl_compliance
           ci_runners_allowed_plans
+          limit_normalized_email_reuse
         ].freeze
 
       CONFIG_FILE_ROOT = 'ee/config/saas_features'
diff --git a/ee/spec/models/ee/user_spec.rb b/ee/spec/models/ee/user_spec.rb
index 481f13b88ca14231e3e99f7d57bb1ae9dba7495b..c2aa78f2768786333d4b8472de7f5e0c4d2ccb93 100644
--- a/ee/spec/models/ee/user_spec.rb
+++ b/ee/spec/models/ee/user_spec.rb
@@ -3643,4 +3643,78 @@
       end
     end
   end
+
+  context 'normalized email reuse check' do
+    let(:error_message) { 'Email is not allowed. Please enter a different email address and try again.' }
+    let(:tumbled_email) { 'user+inbox1@test.com' }
+    let(:normalized_email) { 'user@test.com' }
+
+    subject(:new_user) { build(:user, email: tumbled_email).tap(&:valid?) }
+
+    before do
+      stub_const("::AntiAbuse::UniqueDetumbledEmailValidator::NORMALIZED_EMAIL_ACCOUNT_LIMIT", 1)
+      create(:user, email: normalized_email)
+    end
+
+    context 'when the user has a gitlab.com email address' do
+      let(:tumbled_email) { 'user+inbox1@gitlab.com' }
+      let(:normalized_email) { 'user@gitlab.com' }
+
+      context 'when running in saas', :saas do
+        it 'does not add an error' do
+          expect(new_user.errors).to be_empty
+        end
+      end
+
+      context 'when not running in saas' do
+        it 'adds a validation error' do
+          expect(new_user.errors.full_messages).to include(error_message)
+        end
+      end
+    end
+
+    context 'when a saas user has a an email associated with a verified domain', :saas do
+      let(:verified_domain) { 'verified.com' }
+      let(:tumbled_email) { "user+inbox1@#{verified_domain}" }
+      let(:normalized_email) { "user@#{verified_domain}" }
+
+      context 'when the group is paid' do
+        let_it_be(:ultimate_group) { create(:group_with_plan, plan: :ultimate_plan) }
+        let_it_be(:ultimate_project) { create(:project, group: ultimate_group) }
+
+        it 'does not add an error' do
+          create(:pages_domain, domain: verified_domain, project: ultimate_project)
+
+          expect(new_user.errors).to be_empty
+        end
+      end
+
+      context 'when the root group is not paid' do
+        let_it_be(:free_group) { create(:group) }
+        let_it_be(:free_project) { create(:project, group: free_group) }
+
+        it 'adds a validation error' do
+          create(:pages_domain, domain: verified_domain, project: free_project)
+
+          expect(new_user.errors.full_messages).to include(error_message)
+        end
+      end
+
+      context 'when the root group does not exist' do
+        let_it_be(:project) { create(:project) }
+
+        it 'adds a validation error' do
+          create(:pages_domain, domain: verified_domain, project: project)
+
+          expect(new_user.errors.full_messages).to include(error_message)
+        end
+      end
+    end
+
+    context 'when user is on a self-managed instance' do
+      it 'does not check domain verification' do
+        expect(::PagesDomain).not_to receive(:verified)
+      end
+    end
+  end
 end
diff --git a/lib/gitlab/saas.rb b/lib/gitlab/saas.rb
index 754eed4f25375c5945e89d4cc27c88706dcf5365..847ac5c36de12efe0bce1da5c41b624da628e61d 100644
--- a/lib/gitlab/saas.rb
+++ b/lib/gitlab/saas.rb
@@ -5,6 +5,10 @@
 
 module Gitlab
   module Saas
+    def self.root_domain
+      'gitlab.com'
+    end
+
     def self.com_url
       'https://gitlab.com'
     end
diff --git a/spec/lib/gitlab/saas_spec.rb b/spec/lib/gitlab/saas_spec.rb
index 3be0a6c7bf08ec863b29e58bd483f08fafb1819c..e9132b7a2290d832cdf94529975879f0af940322 100644
--- a/spec/lib/gitlab/saas_spec.rb
+++ b/spec/lib/gitlab/saas_spec.rb
@@ -6,6 +6,12 @@
 RSpec.describe Gitlab::Saas, feature_category: :shared do
   include SaasTestHelper
 
+  describe '.root_domain' do
+    subject { described_class.root_domain }
+
+    it { is_expected.to eq('gitlab.com') }
+  end
+
   describe '.canary_toggle_com_url' do
     subject { described_class.canary_toggle_com_url }
 
diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb
index c21612ae8277f5d7847c80beab4eb6e02b6d0d7e..bdc4945bc729e0c2c24982863d1cc7a573135170 100644
--- a/spec/models/email_spec.rb
+++ b/spec/models/email_spec.rb
@@ -59,6 +59,22 @@
     let_it_be(:unconfirmed_secondary_email) { create(:email, user: confirmed_user) }
     let_it_be(:confirmed_secondary_email) { create(:email, :confirmed, user: confirmed_user) }
 
+    describe '.users_by_detumbled_email_count' do
+      before do
+        # Create users with primary and secondary emails that detumble to the same email: user@example.com.
+        user = create(:user, email: 'user+A@example.com') # New user with a matching primary email
+        create(:user, email: 'user+B@example.com') # New user with a matching primary email
+        create(:email, user: user, email: 'user+C@example.com') # Duplicate user with a matching secondary email
+        create(:email, email: 'user+D@example.com') # New user with a matching secondary email
+      end
+
+      # We created 4 emails but this method should only return a count of 3 since one user
+      # has a primary and secondary email that detumble to the same email address.
+      it 'return the count of unique users with the same detumbled email address' do
+        expect(described_class.users_by_detumbled_email_count('user@example.com')).to eq(3)
+      end
+    end
+
     describe '.confirmed' do
       it 'returns confirmed emails' do
         expect(described_class.confirmed).to contain_exactly(
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 062321a1aa11e9cf2104c4f21a9b2829e368b7f5..f7ae0289e7e024e136ad575361ec7ad4ef271a50 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -8834,51 +8834,148 @@ def owner_class_attribute_default; end
     end
   end
 
-  context 'banned user normalized email reuse check' do
-    let_it_be(:existing_user) { create(:user) }
+  context 'normalized email reuse check' do
+    let(:error_message) { 'Email is not allowed. Please enter a different email address and try again.' }
+
+    subject(:new_user) { build(:user, email: tumbled_email).tap(&:valid?) }
 
-    shared_examples 'does not perform the check' do
+    shared_examples 'adds a validation error' do
       specify do
+        subject
+
+        expect(new_user.errors.full_messages).to include(error_message)
+      end
+    end
+
+    shared_examples 'checking normalized email reuse limit' do
+      before do
+        stub_const("AntiAbuse::UniqueDetumbledEmailValidator::NORMALIZED_EMAIL_ACCOUNT_LIMIT", 2)
+      end
+
+      context 'when the normalized email limit has been reached by unique users' do
+        before do
+          create(:user, email: tumbled_email.split('@').join('1@'))
+        end
+
+        it_behaves_like 'adds a validation error'
+
+        it 'performs the normalized email limit check' do
+          expect(Email).to receive(:users_by_detumbled_email_count).and_call_original
+
+          subject
+        end
+      end
+
+      context 'when the normalized email limit has been reached by non-unique users' do
+        before do
+          user = described_class.find_by(email: normalized_email)
+          create(:email, user: user, email: tumbled_email.split('@').join('1@'))
+        end
+
+        it 'does not add an error' do
+          expect(new_user.errors).to be_empty
+        end
+      end
+
+      context 'when the normalized email limit has not been reached' do
+        it 'does not add an error' do
+          expect(new_user.errors).to be_empty
+        end
+      end
+
+      context 'when the feature flag is disabled' do
+        before do
+          stub_feature_flags(limit_normalized_email_reuse: false)
+        end
+
+        it 'does not perform the check' do
+          expect(Email).not_to receive(:users_by_detumbled_email_count)
+
+          subject
+        end
+      end
+    end
+
+    context 'when email has other validation errors' do
+      subject(:new_user) { build(:user, email: 'invalid-email').tap(&:valid?) }
+
+      it 'does not perform the normalized email checks' do
         expect(::Users::BannedUser).not_to receive(:by_detumbled_email)
+        expect(Email).not_to receive(:users_by_detumbled_email_count)
 
         subject
       end
     end
 
-    context 'when email has other validation errors' do
-      subject(:new_user) { build(:user, email: existing_user.email).tap(&:valid?) }
+    context 'when the email has not changed' do
+      it 'does not perform the normalized email checks' do
+        user = create(:user)
 
-      it_behaves_like 'does not perform the check'
+        expect(::Users::BannedUser).not_to receive(:by_detumbled_email)
+        expect(Email).not_to receive(:users_by_detumbled_email_count)
+
+        user.valid?
+      end
     end
 
     context 'when email has no other validation errors' do
-      let(:error_message) { 'Email is not allowed. Please enter a different email address and try again.' }
-      let(:tumbled_email) { 'person+inbox1@test.com' }
-      let(:normalized_email) { 'person@test.com' }
-      let!(:banned_user) { create(:user, :banned, email: normalized_email) }
+      context 'when the email is associated with a banned user' do
+        let(:tumbled_email) { 'banned+inbox1@test.com' }
+        let(:normalized_email) { 'banned@test.com' }
 
-      subject(:new_user) { build(:user, email: tumbled_email).tap(&:valid?) }
+        before do
+          create(:user, :banned, email: normalized_email)
+        end
 
-      it 'performs the check and adds an error' do
-        subject
+        it_behaves_like 'adds a validation error'
 
-        expect(new_user.errors.full_messages).to include(error_message)
-      end
+        it 'performs the banned user check' do
+          expect(::Users::BannedUser).to receive(:by_detumbled_email).and_call_original
 
-      context 'and does not match normalized email of a banned user' do
-        let(:tumbled_email) { 'unique+tumbled@email.com' }
+          subject
+        end
 
-        it 'does not add an error' do
-          expect(new_user.errors.full_messages).not_to include(error_message)
+        it 'does not perform the normalized email limit check' do
+          expect(Email).not_to receive(:users_by_detumbled_email_count)
+
+          subject
+        end
+
+        context 'and does not match normalized email of a banned user' do
+          let(:tumbled_email) { 'unique+tumbled@email.com' }
+
+          it 'does not add an error' do
+            expect(new_user.errors).to be_empty
+          end
+        end
+
+        context 'when feature flag is disabled' do
+          before do
+            stub_feature_flags(block_banned_user_normalized_email_reuse: false)
+          end
+
+          it 'does not perform the check' do
+            expect(::Users::BannedUser).not_to receive(:by_detumbled_email)
+          end
+
+          it_behaves_like 'checking normalized email reuse limit'
         end
       end
 
-      context 'when feature flag is disabled' do
+      context 'when the email is not associated with a banned user' do
+        let(:tumbled_email) { 'active+inbox1@test.com' }
+        let(:normalized_email) { 'active@test.com' }
+
         before do
-          stub_feature_flags(block_banned_user_normalized_email_reuse: false)
+          create(:user, email: normalized_email)
+        end
+
+        it 'performs the check and does not add an error' do
+          expect(::Users::BannedUser).to receive(:by_detumbled_email).and_call_original
+          expect(new_user.errors).to be_empty
         end
 
-        it_behaves_like 'does not perform the check'
+        it_behaves_like 'checking normalized email reuse limit'
       end
     end
   end