diff --git a/dist/tools/backport_pr/.gitignore b/dist/tools/backport_pr/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..92f3a8ec561361a85e4830f99ed477cbf5b9e437
--- /dev/null
+++ b/dist/tools/backport_pr/.gitignore
@@ -0,0 +1,2 @@
+# tox envs directory
+.tox
diff --git a/dist/tools/backport_pr/README.md b/dist/tools/backport_pr/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..0cc7b48149881412ee306528df1c816dacc20e5b
--- /dev/null
+++ b/dist/tools/backport_pr/README.md
@@ -0,0 +1,77 @@
+Pull requests backport script
+=============================
+
+This script provides functionality to easily backport a merged pull request to
+a release branch.
+
+It relies on having a `github` API token stored in `~/.riotgithubtoken` by
+default.
+
+
+The script works by fetching information from the supplied **merged** pull
+request. It then looks for the last release branch.
+A temporary git `worktree` with a new branch is created based on this release
+branch. All commits from the pull request are then cherry-picked into this
+branch which is then pushed to `origin`.
+It then creates a new pull request on `github` with a reference to the original
+pull request. It optionally puts a comment under the original pull request to
+the new backport pull request.
+
+
+
+Usage
+-----
+
+Most common usage would be to run:
+
+    backport_pr.py --comment PR_NUMBER
+
+
+If you are executing from a worktree, the script cannot currently detect the
+base git directory and must be used with `--gitdir PATH_TO_ORIGINAL_GIT_DIR`
+
+
+Full help:
+
+    usage: backport_pr.py [-h] [-k KEYFILE] [-c] [-n] [-r RELEASE_BRANCH]
+                          [--backport-branch-fmt BACKPORT_BRANCH_FMT] [-d GITDIR]
+                          PR
+
+    positional arguments:
+      PR                    Pull request number to backport
+
+    optional arguments:
+      -h, --help            show this help message and exit
+      -k KEYFILE, --keyfile KEYFILE
+                            File containing github token
+      -c, --comment         Put a comment with a reference underthe original PR
+      -n, --noop            Limited noop mode, creates branch, but doesn'tpush and
+                            create the PR
+      -r RELEASE_BRANCH, --release-branch RELEASE_BRANCH
+                            Base the backport on this branch, default is the
+                            latest
+      --backport-branch-fmt BACKPORT_BRANCH_FMT
+                            Backport branch format. Fields '{release}' and
+                            '{origbranch} will be replaced by the release name and
+                            remote branch name.
+      -d GITDIR, --gitdir GITDIR
+                            Base git repo to work from
+
+
+### Create an authentication token
+
+A `Personal access token` must be created on the `github` website and stored in
+your home directory at `~/.riotgithubtoken`.
+
+The setting is located at https://github.com/settings/tokens :
+
+    Github->Settings->
+        Developper settings->Personal access tokens->Generate New Token
+
+And it should have the following scope:
+
+    repo[public repo]: Access public repositories
+
+
+**Warning** its value should be saved directly as you cannot see it later
+anymore.
diff --git a/dist/tools/backport_pr/backport_pr.py b/dist/tools/backport_pr/backport_pr.py
new file mode 100755
index 0000000000000000000000000000000000000000..91ce72f91fa329e282b3e1aa927dd717ef1e443f
--- /dev/null
+++ b/dist/tools/backport_pr/backport_pr.py
@@ -0,0 +1,225 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2018 Freie Universitat Berlin
+# Copyright (C) 2018 Inria
+#
+# This file is subject to the terms and conditions of the GNU Lesser
+# General Public License v2.1. See the file LICENSE in the top level
+# directory for more details.
+#
+# @author   Koen Zandberg <koen@bergzand.net>
+
+import os
+import os.path
+import sys
+import shutil
+import argparse
+
+import git
+from agithub.GitHub import GitHub
+
+ORG = "RIOT-OS"
+REPO = "RIOT"
+GITHUBTOKEN_FILE = ".riotgithubtoken"
+WORKTREE_SUBDIR = "backport_temp"
+
+RELEASE_PREFIX = ""
+RELEASE_SUFFIX = "-branch"
+
+LABELS_REMOVE = ['Process: needs backport']
+LABELS_ADD = ['Process: release backport']
+
+BACKPORT_BRANCH = 'backport/{release}/{origbranch}'
+
+
+def _get_labels(pr):
+    labels = {label['name'] for label in pr['labels']}
+    for remove in LABELS_REMOVE:
+        labels.discard(remove)
+    labels.update(LABELS_ADD)
+    return sorted(list(labels))
+
+
+def _branch_name_strip(branch_name, prefix=RELEASE_PREFIX,
+                       suffix=RELEASE_SUFFIX):
+    """Strip suffix and prefix.
+
+    >>> _branch_name_strip('2018.10-branch')
+    '2018.10'
+    """
+    if (branch_name.startswith(prefix) and
+            branch_name.endswith(suffix)):
+        if prefix:
+            branch_name = branch_name.split(prefix, maxsplit=1)[0]
+        if suffix:
+            branch_name = branch_name.rsplit(suffix, maxsplit=1)[0]
+    return branch_name
+
+
+def _get_latest_release(branches):
+    version_latest = 0
+    release_fullname = ''
+    release_short = ''
+    for branch in branches:
+        branch_name = _branch_name_strip(branch['name'])
+        branch_num = 0
+        try:
+            branch_num = int(''.join(branch_name.split('.')))
+        except ValueError:
+            pass
+        if branch_num > version_latest:
+            version_latest = branch_num
+            release_short = branch_name
+            release_fullname = branch['name']
+    return (release_short, release_fullname)
+
+
+def _get_upstream(repo):
+    for remote in repo.remotes:
+        if (remote.url.endswith("{}/{}.git".format(ORG, REPO)) or
+                remote.url.endswith("{}/{}".format(ORG, REPO))):
+            return remote
+
+
+def _delete_worktree(repo, workdir):
+    shutil.rmtree(workdir)
+    repo.git.worktree('prune')
+
+
+def main():
+    keyfile = os.path.join(os.environ['HOME'], GITHUBTOKEN_FILE)
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-k", "--keyfile", type=argparse.FileType('r'),
+                        default=keyfile,
+                        help="File containing github token")
+    parser.add_argument("-c", "--comment", action="store_true",
+                        help="Put a comment with a reference under"
+                             "the original PR")
+    parser.add_argument("-n", "--noop", action="store_true",
+                        help="Limited noop mode, creates branch, but doesn't"
+                             "push and create the PR")
+    parser.add_argument("-r", "--release-branch", type=str,
+                        help="Base the backport on this branch, "
+                             "default is the latest")
+    parser.add_argument("--backport-branch-fmt", type=str,
+                        default=BACKPORT_BRANCH,
+                        help="Backport branch format. "
+                             "Fields '{release}' and '{origbranch} will be "
+                             "replaced by the release name and remote branch "
+                             "name.")
+    parser.add_argument('-d', '--gitdir', type=str, default=os.getcwd(),
+                        help="Base git repo to work from")
+    parser.add_argument("PR", type=int, help="Pull request number to backport")
+    args = parser.parse_args()
+
+    gittoken = args.keyfile.read().strip()
+    g = GitHub(token=gittoken)
+    # TODO: exception handling
+    status, user = g.user.get()
+    if status != 200:
+        print("Could not retrieve user: {}".format(user['message']))
+        exit(1)
+    username = user['login']
+    status, pulldata = g.repos[ORG][REPO].pulls[args.PR].get()
+    if status != 200:
+        print("Commit #{} not found: {}".format(args.PR, pulldata['message']))
+        sys.exit(2)
+    if not pulldata['merged']:
+        print("Original PR not yet merged")
+        exit(0)
+    print("Fetching for commit: #{}: {}".format(args.PR, pulldata['title']))
+    orig_branch = pulldata['head']['ref']
+    status, commits = g.repos[ORG][REPO].pulls[args.PR].commits.get()
+    if status != 200:
+        print("No commits found for #{}: {}".format(args.PR,
+                                                    commits['message']))
+        sys.exit(3)
+    for commit in commits:
+        print("found {} : {}".format(commit['sha'],
+                                     commit['commit']['message']))
+
+    # Find latest release branch
+    if args.release_branch:
+        release_fullname = args.release_branch
+        release_shortname = _branch_name_strip(args.release_branch)
+    else:
+        status, branches = g.repos[ORG][REPO].branches.get()
+        if status != 200:
+            print("Could not retrieve branches for {}/{}: {}"
+                  .format(ORG,
+                          REPO,
+                          branches['message']))
+            sys.exit(4)
+        release_shortname, release_fullname = _get_latest_release(branches)
+        if not release_fullname:
+            print("No release branch found, exiting")
+            sys.exit(5)
+    print("Backport based on branch {}".format(release_fullname))
+
+    repo = git.Repo(args.gitdir)
+    # Fetch current upstream
+    upstream_remote = _get_upstream(repo)
+    if not upstream_remote:
+        print("No upstream remote found, can't fetch")
+        exit(6)
+    print("Fetching {} remote".format(upstream_remote))
+
+    upstream_remote.fetch()
+    # Build topic branch in temp dir
+    new_branch = args.backport_branch_fmt.format(release=release_shortname,
+                                                 origbranch=orig_branch)
+    worktree_dir = os.path.join(args.gitdir, WORKTREE_SUBDIR)
+    repo.git.worktree("add", "-b",
+                      new_branch,
+                      WORKTREE_SUBDIR,
+                      "{}/{}".format(upstream_remote, release_fullname))
+    bp_repo = git.Repo(worktree_dir)
+    # Apply commits
+    for commit in commits:
+        bp_repo.git.cherry_pick('-x', commit['sha'])
+    # Push to github
+    print("Pushing branch {} to origin".format(new_branch))
+    if not args.noop:
+        repo.git.push('origin', '{0}:{0}'.format(new_branch))
+    # Delete worktree
+    print("Pruning temporary workdir at {}".format(worktree_dir))
+    _delete_worktree(repo, worktree_dir)
+
+    labels = _get_labels(pulldata)
+    merger = pulldata['merged_by']['login']
+    if not args.noop:
+        # Open new PR on github
+        pr = {
+            'title': "{} [backport {}]".format(pulldata['title'],
+                                               release_shortname),
+            'head': '{}:{}'.format(username, new_branch),
+            'base': release_fullname,
+            'body': "# Backport of #{}\n\n{}".format(args.PR,
+                                                     pulldata['body']),
+            'maintainer_can_modify': True,
+        }
+        status, new_pr = g.repos[ORG][REPO].pulls.post(body=pr)
+        if status != 201:
+            print("Error creating the new pr: \"{}\". Is \"Public Repo\""
+                  " access enabled for the token"
+                  .format(new_pr['message']))
+        pr_number = new_pr['number']
+        print("Create PR number #{} for backport".format(pr_number))
+        g.repos[ORG][REPO].issues[pr_number].labels.post(body=labels)
+        review_request = {"reviewers": [merger]}
+        g.repos[ORG][REPO].pulls[pr_number].\
+            requested_reviewers.post(body=review_request)
+
+    # Put commit under old PR
+    if args.comment and not args.noop:
+        comment = {"body": "Backport provided in #{}".format(pr_number)}
+        status, res = g.repos[ORG][REPO].\
+            issues[args.PR].comments.post(body=comment)
+        if status != 201:
+            print("Something went wrong adding the comment: {}"
+                  .format(res['message']))
+        print("Added comment to #{}".format(args.PR))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/dist/tools/backport_pr/requirements.txt b/dist/tools/backport_pr/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b58a6125479aa41029c583c7e9ec9aa19c647933
--- /dev/null
+++ b/dist/tools/backport_pr/requirements.txt
@@ -0,0 +1,4 @@
+agithub==2.1
+gitdb2==2.0.3
+GitPython==2.1.9
+smmap2==2.0.3
diff --git a/dist/tools/backport_pr/tox.ini b/dist/tools/backport_pr/tox.ini
new file mode 100644
index 0000000000000000000000000000000000000000..5a2ec35680a77f9f6dd23123e843de3b554d8186
--- /dev/null
+++ b/dist/tools/backport_pr/tox.ini
@@ -0,0 +1,32 @@
+[tox]
+envlist = test,lint,flake8
+skipsdist = True
+
+[testenv]
+basepython = python3
+deps = -r {toxinidir}/requirements.txt
+setenv =
+    script = backport_pr.py
+commands =
+    test:       {[testenv:test]commands}
+    lint:       {[testenv:lint]commands}
+    flake8:     {[testenv:flake8]commands}
+
+[testenv:test]
+deps =
+    pytest
+    {[testenv]deps}
+commands =
+    pytest -v --doctest-modules {env:script}
+
+[testenv:lint]
+deps =
+    pylint
+    {[testenv]deps}
+commands =
+    pylint {env:script}
+
+[testenv:flake8]
+deps = flake8
+commands =
+    flake8 --max-complexity=10 {env:script}