diff --git a/keeps/delete_old_feature_flags.rb b/keeps/delete_old_feature_flags.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ddf74dd595289581e7dac7dab83acbfb7a8ea859
--- /dev/null
+++ b/keeps/delete_old_feature_flags.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+require 'cgi'
+require 'httparty'
+require 'json'
+
+module Keeps
+  # This is an implementation of a ::Gitlab::Housekeeper::Keep. This keep will locate any featrure flag definition file
+  # that were added at least `<CUTOFF_MILESTONE_OLD> milestones` ago and remove the definition file.
+  #
+  # You can run it individually with:
+  #
+  # ```
+  # bundle exec gitlab-housekeeper -d \
+  #   -r keeps/delete_old_feature_flags.rb \
+  #   -k Keeps::DeleteOldFeatureFlags
+  # ```
+  # rubocop:disable Gitlab/HTTParty -- Don't use GitLab dependencies
+  # rubocop:disable Gitlab/Json -- Don't use GitLab dependencies
+  class DeleteOldFeatureFlags < ::Gitlab::Housekeeper::Keep
+    CUTOFF_MILESTONE_OLD = 12
+    GREP_IGNORE = [
+      'locale/',
+      'db/structure.sql'
+    ].freeze
+    API_BASE_URI = 'https://gitlab.com/api/v4'
+    ROLLOUT_ISSUE_URL_REGEX = %r{\Ahttps://gitlab\.com/(?<project_path>.*)/-/issues/(?<issue_iid>\d+)\z}
+
+    FeatureFlag = Struct.new(
+      :name,
+      :feature_issue_url,
+      :introduced_by_url,
+      :rollout_issue_url,
+      :milestone,
+      :group,
+      :type,
+      :default_enabled,
+      keyword_init: true
+    )
+
+    def initialize; end
+
+    def each_change
+      each_feature_flag do |feature_flag_yaml_file, feature_flag_definition|
+        feature_flag = FeatureFlag.new(feature_flag_definition)
+
+        if feature_flag.milestone.nil?
+          puts "#{feature_flag.name} has no milestone set!"
+          next
+        end
+
+        next unless before_cuttoff?(feature_flag.milestone)
+
+        # feature_flag_default_enabled = feature_flag_definition[:default_enabled]
+        # feature_flag_gitlab_com_state = fetch_gitlab_com_state(feature_flag.name)
+
+        # TODO: Handle the different cases of default_enabled vs enabled/disabled on GitLab.com
+
+        # # Finalize the migration
+        title = "Delete the `#{feature_flag.name}` feature flag introduced in #{feature_flag.milestone}"
+
+        identifiers = [self.class.name.demodulize, feature_flag.name]
+
+        # rubocop:disable Gitlab/DocUrl -- Not running inside rails application
+        description = <<~MARKDOWN
+        This feature flag was introduced more than #{CUTOFF_MILESTONE_OLD} milestones ago.
+
+        As part of our process we want to ensure [feature flags don't stay too long in the codebase](https://docs.gitlab.com/ee/development/feature_flags/#types-of-feature-flags).
+
+        <details><summary>Mentions of the feature flag (click to expand)</summary>
+
+        ```
+        #{feature_flag_grep(feature_flag.name)}
+        ```
+
+        </details>
+
+        Labels to set (not yet automated): ~"#{feature_flag.group}"
+
+        This merge request was created using the
+        [gitlab-housekeeper](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/139492)
+        gem.
+        #{assign_command(feature_flag.rollout_issue_url)}
+        MARKDOWN
+        # rubocop:enable Gitlab/DocUrl
+
+        FileUtils.rm(feature_flag_yaml_file)
+
+        changed_files = [feature_flag_yaml_file]
+
+        to_create = ::Gitlab::Housekeeper::Change.new(identifiers, title, description, changed_files)
+        yield(to_create)
+      end
+    end
+
+    def before_cuttoff?(milestone)
+      Gem::Version.new(milestone) < Gem::Version.new(milestone_ago(current_milestone, CUTOFF_MILESTONE_OLD))
+    end
+
+    def fetch_gitlab_com_state(feature_flag_name)
+      # TBD
+    end
+
+    def feature_flag_grep(feature_flag_name)
+      Gitlab::Housekeeper::Shell.execute(
+        'git',
+        'grep',
+        '--heading',
+        '--line-number',
+        '--break',
+        feature_flag_name,
+        '--',
+        *(GREP_IGNORE.map { |path| ":^#{path}" })
+      )
+    end
+
+    def current_milestone
+      milestone = File.read(File.expand_path('../VERSION', __dir__))
+      milestone.gsub(/^(\d+\.\d+).*$/, '\1').chomp
+    end
+
+    def milestone_ago(milestone, num_milestones)
+      major, minor = milestone.split(".").map(&:to_i)
+
+      older_major =
+        if minor >= num_milestones
+          major
+        else
+          major - (((num_milestones - minor - 1) / 13) + 1)
+        end
+
+      older_minor = (0..12).to_a[(minor - num_milestones) % 13]
+
+      [older_major, older_minor].join(".")
+    end
+
+    def assign_command(rollout_issue_url)
+      rollout_issue = get_rollout_issue(rollout_issue_url)
+      return unless rollout_issue
+
+      "/assign #{assignees(rollout_issue)}"
+    end
+
+    def assignees(rollout_issue)
+      rollout_issue[:assignees].map { |assignee| "@#{assignee[:username]}" }.join(' ')
+    end
+
+    def get_rollout_issue(rollout_issue_url)
+      matches = ROLLOUT_ISSUE_URL_REGEX.match(rollout_issue_url)
+      return unless matches
+
+      response = HTTParty.get(
+        "#{API_BASE_URI}/projects/#{CGI.escape(matches[:project_path])}/issues/#{matches[:issue_iid]}"
+      )
+
+      unless (200..299).cover?(response.code)
+        raise Error,
+          "Failed with response code: #{response.code} and body:\n#{response.body}"
+      end
+
+      JSON.parse(response.body, symbolize_names: true)
+    end
+
+    def each_feature_flag
+      all_feature_flag_files.map do |f|
+        yield(f, YAML.load_file(f, permitted_classes: [Symbol], symbolize_names: true))
+      end
+    end
+
+    def all_feature_flag_files
+      Dir.glob("{,ee/}config/feature_flags/{development,gitlab_com_derisk}/*.yml")
+    end
+  end
+end
+# rubocop:enable Gitlab/Json
+# rubocop:enable Gitlab/HTTParty