diff --git a/Documentation/git-branch.txt b/Documentation/git-branch.txt
index d382ac69f7..99cd0486dc 100644
--- a/Documentation/git-branch.txt
+++ b/Documentation/git-branch.txt
@@ -13,6 +13,8 @@ SYNOPSIS
 	[--column[=<options>] | --no-column] [--sort=<key>]
 	[--merged [<commit>]] [--no-merged [<commit>]]
 	[--contains [<commit>]] [--no-contains [<commit>]]
+	[--has-upstream | --no-has-upstream]
+	[--gone | --no-gone]
 	[--points-at <object>] [--format=<format>]
 	[(-r | --remotes) | (-a | --all)]
 	[--list] [<pattern>...]
@@ -325,6 +327,19 @@ superproject's "origin/main", but tracks the submodule's "origin/main".
 	detached HEAD (if present) first, then local branches and
 	finally remote-tracking branches. See linkgit:git-config[1].
 
+--has-upstream::
+	Only list branches that track an upstream branch. Implies `--list`.
+
+--no-has-upstream::
+	Only list branches that do not track an upstream branch. Implies `--list`.
+
+--gone::
+	Only list branches that track a gone upstream branch. Implies `--list` and
+	`--has-upstream`.
+
+--no-gone::
+	Only list branches that do not track a gone upstream branch. Implies
+	`--list`.
 
 --points-at <object>::
 	Only list branches of the given object.
diff --git a/builtin/branch.c b/builtin/branch.c
index f63fd45edb..5cac6dc3c6 100644
--- a/builtin/branch.c
+++ b/builtin/branch.c
@@ -680,6 +680,14 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		OPT__FORCE(&force, N_("force creation, move/rename, deletion"), PARSE_OPT_NOCOMPLETE),
 		OPT_MERGED(&filter, N_("print only branches that are merged")),
 		OPT_NO_MERGED(&filter, N_("print only branches that are not merged")),
+		OPT_SET_INT_F(0, "has-upstream", &filter.has_upstream,
+			      N_("print only branches that track an upstream branch"), 1, PARSE_OPT_NONEG),
+		OPT_SET_INT_F(0, "no-has-upstream", &filter.has_upstream,
+			      N_("print only branches that do not track an upstream branch"), -1, PARSE_OPT_NONEG),
+		OPT_SET_INT_F(0, "gone", &filter.upstream_gone,
+			      N_("print only branches that track a gone upstream branch"), 1, PARSE_OPT_NONEG),
+		OPT_SET_INT_F(0, "no-gone", &filter.upstream_gone,
+			      N_("print only branches that do not track a gone upstream branch"), -1, PARSE_OPT_NONEG),
 		OPT_COLUMN(0, "column", &colopts, N_("list branches in columns")),
 		OPT_REF_SORT(&sorting_options),
 		OPT_CALLBACK(0, "points-at", &filter.points_at, N_("object"),
@@ -719,7 +727,8 @@ int cmd_branch(int argc, const char **argv, const char *prefix)
 		list = 1;
 
 	if (filter.with_commit || filter.no_commit ||
-	    filter.reachable_from || filter.unreachable_from || filter.points_at.nr)
+	    filter.reachable_from || filter.unreachable_from ||
+	    filter.points_at.nr || filter.has_upstream || filter.upstream_gone)
 		list = 1;
 
 	noncreate_actions = !!delete + !!rename + !!copy + !!new_upstream +
diff --git a/ref-filter.c b/ref-filter.c
index f8203c6b05..a0f629bbf7 100644
--- a/ref-filter.c
+++ b/ref-filter.c
@@ -2358,6 +2358,42 @@ void ref_array_clear(struct ref_array *array)
 	}
 }
 
+static void do_tracking_filter(struct ref_filter_cbdata *ref_cbdata)
+{
+	struct ref_filter *filter = ref_cbdata->filter;
+	struct ref_array *array = ref_cbdata->array;
+	int i, old_nr;
+
+	old_nr = array->nr;
+	array->nr = 0;
+
+	for (i = 0; i < old_nr; i++) {
+		struct ref_array_item *item = array->items[i];
+		const char *branch_name = item->refname;
+		struct branch *branch;
+		int num_ours, num_theirs, gone;
+		const char *base;
+
+		skip_prefix(branch_name, "refs/heads/", &branch_name);
+		branch = branch_get(branch_name);
+		gone = stat_tracking_info(branch, &num_ours, &num_theirs,
+					  &base, 0, AHEAD_BEHIND_QUICK) < 0;
+
+		if (filter->has_upstream == 1 && !base)
+			goto remove;
+		if (filter->has_upstream == -1 && base)
+			goto remove;
+		if (filter->upstream_gone == 1 && (!base || !gone))
+			goto remove;
+		if (filter->upstream_gone == -1 && base && gone)
+			goto remove;
+		array->items[array->nr++] = array->items[i];
+		continue;
+remove:
+		free_array_item(item);
+	}
+}
+
 #define EXCLUDE_REACHED 0
 #define INCLUDE_REACHED 1
 static void reach_filter(struct ref_array *array,
@@ -2466,6 +2502,9 @@ int filter_refs(struct ref_array *array, struct ref_filter *filter, unsigned int
 	clear_contains_cache(&ref_cbdata.contains_cache);
 	clear_contains_cache(&ref_cbdata.no_contains_cache);
 
+	if (filter->has_upstream || filter->upstream_gone)
+		do_tracking_filter(&ref_cbdata);
+
 	/*  Filters that need revision walking */
 	reach_filter(array, filter->reachable_from, INCLUDE_REACHED);
 	reach_filter(array, filter->unreachable_from, EXCLUDE_REACHED);
diff --git a/ref-filter.h b/ref-filter.h
index aa0eea4ecf..3d0b321a16 100644
--- a/ref-filter.h
+++ b/ref-filter.h
@@ -57,6 +57,9 @@ struct ref_filter {
 	struct commit_list *reachable_from;
 	struct commit_list *unreachable_from;
 
+	int has_upstream;
+	int upstream_gone;
+
 	unsigned int with_commit_tag_algo : 1,
 		match_as_path : 1,
 		ignore_case : 1,
diff --git a/t/t3208-branch-tracking-filter.sh b/t/t3208-branch-tracking-filter.sh
new file mode 100755
index 0000000000..51e9453ffb
--- /dev/null
+++ b/t/t3208-branch-tracking-filter.sh
@@ -0,0 +1,76 @@
+#!/bin/sh
+
+test_description='branch tracking filter options'
+
+. ./test-lib.sh
+
+test_expect_success setup '
+	git init --initial-branch=tracked-present r1 &&
+	git -C r1 commit --allow-empty -m "Initial commit" &&
+	git -C r1 branch upstream-only &&
+	git -C r1 branch untracked &&
+	git clone r1 r2 &&
+	cd r2 &&
+	git checkout -b tracked-gone &&
+	git push --set-upstream origin tracked-gone &&
+	git push origin :tracked-gone &&
+	git branch --no-track untracked &&
+	git branch downstream-only
+'
+
+test_expect_success 'all local branches' '
+	git branch >actual &&
+	cat >expect <<-\EOF &&
+	  downstream-only
+	* tracked-gone
+	  tracked-present
+	  untracked
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'branch --has-upstream' '
+	git branch --has-upstream >actual &&
+	cat >expect <<-\EOF &&
+	* tracked-gone
+	  tracked-present
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'branch --no-has-upstream' '
+	git branch --no-has-upstream >actual &&
+	cat >expect <<-\EOF &&
+	  downstream-only
+	  untracked
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'branch --gone' '
+	git branch --gone >actual &&
+	cat >expect <<-\EOF &&
+	* tracked-gone
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'branch --no-gone' '
+	git branch --no-gone >actual &&
+	cat >expect <<-\EOF &&
+	  downstream-only
+	  tracked-present
+	  untracked
+	EOF
+	test_cmp expect actual
+'
+
+test_expect_success 'branch --has-upstream --no-gone' '
+	git branch --has-upstream --no-gone >actual &&
+	cat >expect <<-\EOF &&
+	  tracked-present
+	EOF
+	test_cmp expect actual
+'
+
+test_done
